#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time    : 2018/7/11 21:48
# @Author  : hyang
# @File    : main.py
# @Software: python_utils

import socket
import configparser
import struct
import json
import hashlib
import os
import platform
import time
from conf import settings
from utils.single_log import SingletonLog
from utils import cmd_utils
from utils import file_util


class FTPServer():
    """
    处理客户端所有的交互socket server
    """
    STATUS = {
        200: 'Passed authentication ! ',
        201: 'wrong username or password!',
        202: 'wrong username !',
        300: 'File not exist !',
        301: 'File  exist , and the msg include the file size!',
        302: 'File not exist !!!',
        401: 'This msg include the msg size',
        402: 'This msg is Null',
        501: 'dir change',
        502: 'dir is not exist !!!',
        601: 'file exist ready re-send !!!',
        602: "file exist ready ,but file size doesn't match!!!"
    }

    def __init__(self, manage_instance):
        self.manage_instance = manage_instance
        self.logger = SingletonLog().get_logger()
        self.sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.IP_PORT = (settings.HOST, settings.PORT)
        self.sock_server.bind(self.IP_PORT)
        self.sock_server.listen(settings.MAX_SOCKET_LISTEN)
        self.account = self.load_accounts()
        self.user_info = None
        self.conn = None
        self.client_addr = None
        self.user_current_dir = None

    def run_forever(self):
        """
        启动socket
        :return:
        """
        print('starting LuffyFTP Server on %s-%s '.center(50, '-') %
              (settings.HOST, settings.PORT))

        while True:
            self.conn, self.client_addr = self.sock_server.accept()
            print('connect by ', self.client_addr)
            self.handle()

    def handle(self):
        """
        处理用户命令交互
        :return:
        """
        while True:
            try:
                # 收到报头长度
                recv_pack = self.conn.recv(4)
                if not recv_pack:
                    self.logger.warn(
                        'connect {} is lost ……'.format(
                            self.client_addr))
                    # del self.conn, self.client_addr  # 退出时，删除连接
                    break
                # 解析报头
                recv_length = struct.unpack('i', recv_pack)[0]

                header_data = self.conn.recv(recv_length)
                # json_data
                json_data = json.loads(header_data.decode('utf-8'))
                self.logger.info('recv data >>> {}'.format(json_data))
                action_type = json_data.get('action_type')
                if action_type:
                    # 使用反射
                    if hasattr(self, '_{}'.format(action_type)):
                        func = getattr(self, '_{}'.format(action_type))
                        func(json_data)
                else:
                    self.logger.warn('invalid command')
            except ConnectionResetError:  # 适用于windows操作系统
                break
            except Exception as e:
                print('Error happen with client ,close connection', e)

        if hasattr(self, 'conn'):
            self.conn.close()

    @staticmethod
    def load_accounts():
        """
        载入用户配置
        :return:
        """
        config_obj = configparser.ConfigParser()
        config_obj.read(settings.ACCOUNT_FILE)

        return config_obj

    def send_response(self, status_code, **kwargs):
        """
         向客户端发送响应吗
        :param status:
        :return:
        """
        # 构造消息头
        message = {
            'status': status_code,
            'status_message': self.STATUS.get(status_code)
        }
        message.update(kwargs)  # 更新消息
        message_json = json.dumps(message)
        # 为防止粘包，封装消息包
        header_byte = message_json.encode('utf-8')
        # 先发送报头的长度
        self.conn.send(struct.pack('i', len(message_json)))
        self.logger.info('发送response报头的长度: {}'.format(len(message_json)))
        self.logger.info('发送response报头内容:{}'.format(message))
        # 发送报头
        self.conn.send(header_byte)

    def _auth(self, data):
        """
        处理用户请求
        :return:
        """
        self.logger.debug('_auth {}'.format(data))
        # 发送状态码
        self.send_response(
            self.authenticate(
                data.get('username'),
                data.get('password')))

    def authenticate(self, username, password):
        """
        用户密码md5认证
        :param username:
        :param password:
        :return:
        """
        if username in self.account.sections():
            _password = self.account[username]['password']
            md5_obj = hashlib.md5()
            md5_obj.update(password.encode('utf-8'))
            md5_pass = md5_obj.hexdigest()  # md5加密串
            self.logger.info(md5_pass)
            if _password == md5_pass:
                print(self.STATUS.get(200))
                self.user_info = self.account[username]
                # 得到用户home
                home_dir = os.path.join(settings.USER_HOME_DIR, username)
                if not os.path.exists(home_dir):
                    os.makedirs(home_dir)
                self.user_info['home'] = home_dir
                self.user_info['username'] = username
                self.user_current_dir = home_dir

                return 200
            else:
                print(self.STATUS.get(201))
                return 201
        else:
            print(self.STATUS.get(202))
            return 202

    def _put(self, data):
        """
         拿到文件名和大小，检测本地是否存在相同文件
         如果存在，创建新文件local_file_name+timestamp
         如果存在，创建新文件local_file_name
        :param data:
        :return:
        """
        self.logger.debug('_put {}'.format(data))
        file_size = data.get('file_size')
        file_name = data.get('file_name')
        file_path = os.path.join(
            self.user_info.get('home'),
            file_name)
        client_md5 = data.get('md5')
        if os.path.isfile(file_path):
            self.logger.debug('file is exist')
            file_path = '{}.{}'.format(file_path, str(int(time.time())))
        tmp_file = '{}.down'.format(file_path)

        f = open(tmp_file, 'wb')
        recv_size = 0
        print('put file {} start >>> '.format(file_path))
        while recv_size < file_size:
            data = self.conn.recv(8192)  # 接收文件内容
            f.write(data)
            recv_size += len(data)
        else:
            print("\n")
            print(
                '-- file [{}] put done, received size [{}]'.format(file_name, file_util.bytes2human(os.path.getsize(tmp_file))))
        f.close()

        os.rename(tmp_file, file_path)
        server_md5 = file_util.get_md5(file_path)
        if server_md5 == client_md5:
            print('文件上传完整与客户端一致')

    def _get(self, data):
        """
        下载文件，如果文件存在，发送状态码+文件大小+md5，发送文件
        不存在，发送状态码
        :param data:
        :return:
        """
        self.logger.debug('_get {}'.format(data))
        file_path = os.path.join(
            self.user_current_dir,
            data.get('file_name'))
        if os.path.isfile(file_path):
            file_size = os.path.getsize(file_path)
            self.logger.debug(
                'file_path: {} file_size: {} '.format(
                    file_path, file_size))
            self.send_response(301, file_size=file_size, md5=file_util.get_md5(
                file_path), server_file_dir=os.path.dirname(file_path))
            print('read to send file >>>', data.get('file_name'))
            with open(file_path, 'rb') as f:
                for line in f:
                    self.conn.send(line)
                else:
                    print('send file {} done'.format(file_path))

        else:
            self.send_response(302)

    def _re_get(self, data):
        """
        re_send file to client
        1.拼接文件路径
        2.如果文件存在，告诉客户端，续传文件
        :param data:
        :return:
        """
        self.logger.debug('re_get {}'.format(data))
        filename = data.get('filename')
        server_path = data.get('server_file_dir')
        full_path = os.path.join(server_path, filename)
        if os.path.isfile(full_path):
            if os.path.getsize(full_path) == data.get('file_size'):  # 判断文件大小
                self.send_response(601)
                f = open(full_path, 'rb')
                f.seek(data.get('recv_size'))
                for line in f:
                    self.conn.send(line)
                else:
                    print('file re_send done'.center(50, '-'))
                    f.close()
            else:
                self.send_response(
                    602, file_size_on_server=os.path.getsize(full_path))

        else:
            self.send_response(300)

    def _ls(self, data):
        """
        返回dir或ls的结果
        :param data:
        :return:
        """
        if platform.system() == 'Windows':
            res = cmd_utils.cmd_exec('dir {}'.format(self.user_current_dir))
        else:
            res = cmd_utils.cmd_exec('ls {}'.format(self.user_current_dir))

        if res:
            # 发送内容长度和msg
            self.send_response(401, res_size=len(res))
            # 发送内容
            self.conn.sendall(res)
        else:
            self.send_response(402)

    def _cd(self, data):
        """
        改变用户的self.current_dir
        1. target_dir与self.current_dir进行拼接
        2. 检测切换的目录是否存在
        3. 改变self.current_dir到新的值
        :param data:
        :return:
        """
        target_dir = data.get('target_dir')
        target_dir = os.path.abspath(
            os.path.join(
                self.user_current_dir,
                target_dir))  # 解决../问题
        self.logger.debug("full path {}".format(target_dir))
        if os.path.isdir(target_dir):
            if target_dir.startswith(self.user_info['home']):  # has permission
                self.user_current_dir = target_dir  # 切换当前目录
                self.relative_dir = self.user_current_dir.replace(
                    self.user_info['home'], '')
                self.send_response(
                    501, current_dir=self.relative_dir)  # 发送相对目录
            else:
                self.send_response(502)
        else:
            self.send_response(502)


if __name__ == '__main__':
    pass
